﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using X3DXNA.utils;

namespace X3DXNA.core
{
    class Camera
    {

        #region CONSTANTS

        private const float InputTurnRate = 2.0f;
        private const float InputMoveRate = 10.0f;

        #endregion

        #region Private Fields

        BoundingFrustum boundingFrustum;
        Game game;
        Matrix View, Projection;
        Vector3 _position;
        Quaternion _quaternionOrientation;
        float _fieldOfView;

        #endregion

        #region Public Properties

        public Matrix viewMatrix { get { return View; } set { View = value; recalculateBoundingFrustum(); } }
        public Matrix projectionMatrix { get { return Projection; } set { Projection = value; recalculateBoundingFrustum(); } }

        public Vector3 position
        {
            get { recalculatePositionAndOrientation(); return _position; }
            set
            {
                _position = value;
                recalculateView();
                recalculateBoundingFrustum();
            }
        }
        public Vector4 orientation {
            set
            {
                _quaternionOrientation = utility.vector4ToQuaternionRotation(value);
                recalculateView();
                recalculateBoundingFrustum();
            }
        }
        public Quaternion quaternionOrientation { get { recalculatePositionAndOrientation(); return _quaternionOrientation; } set { _quaternionOrientation = value; recalculateView(); recalculateBoundingFrustum(); } }

        public float fieldOfView { get { return _fieldOfView; }
            set
            {
                _fieldOfView = value;
                recalculateProjection();
                recalculateBoundingFrustum();
            }
        }

        #endregion

        #region Constructor

        public Camera(Game game, Vector3 position, Vector4 orientation, float fieldOfView)
        {
            //Add this camera to the services.
            game.Services.AddService(typeof(Camera), this);
            this.game = game;

            setPositionAxisOrientationAndFieldOfView(position, orientation, fieldOfView);
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Setta la Posizione, l'orientamento e il Field Of View della camera.
        /// </summary>
        public void setPositionAxisOrientationAndFieldOfView(Vector3 position, Vector4 orientation, float fieldOfView)
        {
            this._position = position;
            this._quaternionOrientation = utility.vector4ToQuaternionRotation(orientation);
            var direction = _position - new Vector3(0, 0, 1);

            View = Matrix.CreateLookAt(_position, direction, Vector3.Up);
            View *= Matrix.CreateFromQuaternion(_quaternionOrientation);

            Projection = Matrix.CreatePerspectiveFieldOfView(
                MathHelper.ToRadians(45),
                (float)game.GraphicsDevice.Viewport.Width / (float)game.GraphicsDevice.Viewport.Height,
                0.1f,
                100.0f
            );

            recalculateBoundingFrustum();
        }

        /// <summary>
        /// Setta la posizione e l'orientamento della camera.
        /// </summary>
        public void setPositionAndQuaternionOrientation(Vector3 position, Quaternion orientation)
        {
            this._position = position;
            this._quaternionOrientation = orientation;
            var direction = _position - new Vector3(0, 0, 1);
            View = Matrix.CreateLookAt(_position, direction, Vector3.Up);
            View *= Matrix.CreateFromQuaternion(orientation);

            recalculateBoundingFrustum();
        }

        /// <summary>
        /// Ottiene l'attuale boundingFrustum della camera.
        /// </summary>
        public BoundingFrustum getBoundingFrustum()
        {
            return boundingFrustum;
        }

        public void HandleDefaultKeyboardControls(KeyboardState kbState, GameTime gameTime)
        {
            if (gameTime == null)
            {
                throw new ArgumentNullException("gameTime",
                    "GameTime parameter cannot be null.");
            }

            float elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;

            float dX = elapsedTime * ReadKeyboardAxis(
                kbState, Keys.Left, Keys.Right) * InputTurnRate;
            float dY = elapsedTime * ReadKeyboardAxis(
                kbState, Keys.Up, Keys.Down) * InputTurnRate;
            float mX = elapsedTime * ReadKeyboardAxis(
                kbState, Keys.A, Keys.D) * InputMoveRate;
            float mY = elapsedTime * ReadKeyboardAxis(
                kbState, Keys.W, Keys.S) * InputMoveRate;
            float mZ = elapsedTime * ReadKeyboardAxis(
                kbState, Keys.Space, Keys.LeftControl) * InputMoveRate;

            if (dX != 0)
                viewMatrix *= Matrix.CreateFromQuaternion(Quaternion.CreateFromAxisAngle(View.Up, dX));
            if (dY != 0)
                viewMatrix *= Matrix.CreateFromQuaternion(Quaternion.CreateFromAxisAngle(Vector3.Left, dY));
            if (mX != 0)
                viewMatrix *= Matrix.CreateTranslation(Vector3.Left * mX);
            if (mY != 0)
                viewMatrix *= Matrix.CreateTranslation(Vector3.Forward * mY);
            if (mZ != 0)
                viewMatrix *= Matrix.CreateTranslation(Vector3.Up * mZ);
        }

        #endregion

        #region Private Methods

        private void recalculateView()
        {
            var direction = _position - new Vector3(0, 0, 1);
            View = Matrix.CreateLookAt(_position, direction, Vector3.Up);
            View *= Matrix.CreateFromQuaternion(_quaternionOrientation);
            recalculateBoundingFrustum();
        }

        private void recalculateBoundingFrustum()
        {
            boundingFrustum = new BoundingFrustum(View * Projection);
        }

        private void recalculatePositionAndOrientation()
        {
            Vector3 garbage1, garbage2;
            Quaternion orientation;
            View.Decompose(out garbage1, out orientation, out garbage2);
            _quaternionOrientation = orientation;
            Matrix viewIT = Matrix.Invert(Matrix.Transpose(View));
            Vector3 position = new Vector3(viewIT.M14, viewIT.M24, viewIT.M34);
            _position = position;
        }

        private void recalculateProjection()
        {
            Projection = Matrix.CreatePerspectiveFieldOfView(
                MathHelper.ToRadians(45),
                (float)game.GraphicsDevice.Viewport.Width / (float)game.GraphicsDevice.Viewport.Height,
                0.1f,
                100.0f
            );
        }

        private static float ReadKeyboardAxis(KeyboardState keyState, Keys downKey, Keys upKey)
        {
            float value = 0;

            if (keyState.IsKeyDown(downKey))
                value -= 1.0f;

            if (keyState.IsKeyDown(upKey))
                value += 1.0f;

            return value;
        }

        #endregion

    }
}
